在處理響應式多選表單時,使用 v-model 綁定陣列類型的資料,用 ref()
沒問題,但用 reactive()
卻行不通QQ~為什麼~~~
就是下面這個範例:
當初在響應式篇章,明明說 object 和 array 可以用 reactive()
來做響應的?
我就很開心的把 ref()
改成 reative()
了!
結果大踩坑,Vue 不會報錯或警告,但資料就是沒辦法響應。
這裡為懶得打開 Vue 開發環境的人做了個線上 DEMO 範例。
總之,我和讀書會夥伴就踏上了找尋真相的路途~!
在進入原始碼之前,想拋出個問題讓大家想想,這也會是等等研究原碼的重點。
『你有想過 v-model 是透過「改動原陣列」還是「創造新陣列」的方式來更新值嗎?』
v-model 是 Vue 提供的雙向綁定指令,在每次觸發事件時,會「更新」綁定的變數。
為什麼要強調「更新」?這跟 Javascript 的型別有關。
const text = ref("")
<input v-model="text">
<input
:value="text"
@input="event => text = event.target.value">
[]
[]
呢?在進入原始碼之前,要不要先猜猜看,v-model 是透過「改動原陣列」還是「創造新陣列」的方式來更新值?
會針對 vModelCheckbox
-- v-model 綁定 checkbox 的部份做分析,完整原始碼連結
先看一下使用情境:
const checkedNames = ref([])
<div>Checked names: {{ checkedNames }}</div>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
// #4096 array checkboxes need to be deep traversed
deep: true,
created(el, _, vnode) {
el._assign = getModelAssigner(vnode)
//調用 `assign` 時會把新的 modelValue 傳進去
//這個 method 比較複雜,可以先簡單想成更新 modelValue 的值
addEventListener(el, 'change', () => {
const modelValue = (el as any)._modelValue //v-model 綁定的值稱為 modelValue
const elementValue = getValue(el) //監聽元素 checkbox 所綁定的 value 值
const checked = el.checked //checkbox 是否被打勾
const assign = el._assign
//如果綁定的 modelValue 是 Array
if (isArray(modelValue)) {
const index = looseIndexOf(modelValue, elementValue)
//找 checkbox 的 value 是否在 modelValue 陣列內*
//*底下會補充為何不直接用原生的 indexOf
const found = index !== -1
//在 modelvalue 陣列內是否有找到對應的 value
//元素被打勾 && modelValue 內沒有 => 表示新打勾的
if (checked && !found) {
assign(modelValue.concat(elementValue))
//元素沒有被打勾 && modelValue 內有 => 表示取消打勾
} else if (!checked && found) {
const filtered = [...modelValue]
filtered.splice(index, 1)
assign(filtered)
}
//如果綁定的 modelValue 是 Set,以下不討論
} else if (isSet(modelValue)) {
//略,今天不討論
} else {
//綁定的 modelValue 不是 Array 或 Set => 單選 checkbox
assign(getCheckboxValue(el, checked))
}
})
},
mounted: setChecked,
beforeUpdate(el, binding, vnode) {
el._assign = getModelAssigner(vnode)
setChecked(el, binding, vnode)
}
}
補充說明:
looseIndex
indexOf
,是因為 Vue 支援 checkbox value 綁定非基本型別(如:物件),而原生 Javascript 方法只能判斷物件的參照是否相同,不能判斷物件內的值是否相同,所以要調用 Vue 自己定義的 looseIndexOf
函式,來比較物件項目的內部值是否相同。前面有提到,要特別研究:v-model 如何處理「陣列型別 modelValue 的更新」
主要相關的程式碼是這段:
//元素被打勾 && modelValue 內沒有 => 表示新打勾的
if (checked && !found) {
assign(modelValue.concat(elementValue))
//元素沒有被打勾 && modelValue 內有 => 表示取消打勾
} else if (!checked && found) {
const filtered = [...modelValue]
filtered.splice(index, 1)
assign(filtered)
}
這裡調用的方法相信大家都不陌生,不管是新增或刪除,v-model 都不會直接更動原本的陣列(modelValue),而是創造一個指向不同參照的新陣列。
Array.prototype.concat()
會合併並回傳新陣列,不會影響原陣列[...modelValue]
透過解構賦值展開,可以拿到一個新陣列Array.prototype.splice()
直接修改新陣列也就是說,v-model 是透過重新賦值一個新陣列,來更新 modelValue 的值。
ref
&reactive
的關係大家還記得 ref
和 reactive
的特性嗎?
長話短說~reactive()
是利用 Proxy 來實作,所以 reactive
物件不能被重新賦值,這也是 v-model 多選綁定 reactive([])
會失效的原因。
註1: RefImpl.value
值指向物件依然可以被重新賦值
註2: 還不清楚 ref
和 reactive
的特性的人可以參考之前的文章,這裡就不再贅述。
Day 10: 從原生 JS 理解 Vue 3 響應式基礎 - reactive & ref (上)
Day 11: 從原生 JS 理解 Vue 3 響應式基礎 - reactive & ref (下)
不能用 reactive()
做 v-model 陣列的響應,這難道不是個 bug 嗎?
我們在 vue.js 的 github 上找到一串相關 issue。
vue.js 一度採納 issue 提案,讓 v-model 多選陣列能支援 reactive()
響應,但後來發現,這項變動的負面成本比預期的還大,所以幾乎是一天內又改回來了。
透過 reactive()
響應的資料不可被重新賦值,也就是說,如果 v-model 要支援 reactive
陣列,必須直接在 v-model 中改動(mutate) 綁定的陣列。
其中一個最大的影響就是,v-model + computed 的搭配用法會失效,modelValue 值的更新會繞過 computed 的 setter!
為什麼?
這樣操作會直接改動原本的陣列,而且陣列參照沒有改變,不會無法觸發 computed 的 setter。
v-model + computed 的搭配使用法參考如下:
現在的 Vue 是透過重新賦值來更新陣列,所以下面這個用法是沒問題的!
<input v-model="inputValue">
const inputValue = computed({
get: () => props.value,
set: newValue => emit('update:value', newValue)
})
})
如果要在現行 Vue 版本下模擬相似的情境,我覺得可以參考下面的程式碼,線上 DEMO 連結:
const arr = reactive([]);
const computedArr = computed({
get() {
window.alert(`getting`)
return arr
},
set(newValue) {
window.alert(`getting`)
//其實也不需要了,因為陣列直接被改動了
arr = newValue
},
})
<button @click="computedArr.push("Jack")">push</button>
<p>arr:{{ arr }}</p>
<p>computedArr:{{ computedArr }}</p>
我是用點擊觸發 computedArr.push("Jack")
模擬 v-model 直接操作 reactive 陣列的部份,每次 push
,原始的 arr
會直接被改動,而且每次改動陣列都無法觸發 computedArr
的 setter。
參考 issue: Checkbox v-model array mutation#2700
reactive()
物件不能重新賦值,陣列型別的 modelValue 只能透過 ref()
來做響應reactive()
並不是 bug,是為了避免在 v-model 中改動原陣列,會導致非預期情況出現,負面效益大於正面效益以上是我和讀書會夥伴們研究的結果。
我自己覺得有點難解釋,如果有什麼說明不清楚的地方,歡迎留言告訴我~
有什麼不同的想法,或發現有解讀不恰當的地方,也歡迎留言一起討論!
(但我可能要鐵人賽後才會出現回覆QQ)
最後,感謝耐心看到最後的你~
對於 v-model 和原生比較,可以參考下列文章:
Day27 前端蛇行撞牆記 - 表單輸入 input 原生及 v-model 比較 / Vue 3 (上)
Day28 前端蛇行撞牆記 - input 原生及 v-model 比較 / Vue 3 (下)